{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "95a87145-34d0-4f97-b45f-5c9fd8532c8a",
   "metadata": {},
   "source": [
    "# How to create map-reduce branches for parallel execution\n",
    "\n",
    "A common pattern in agents is to generate a list of objects, do some work on each of those objects, and then combine the results. This is very similar to the common [map-reduce](https://en.wikipedia.org/wiki/MapReduce) operation. This can be tricky for a few reasons. First, it can be tough to define a structured graph ahead of time because the length of the list of objects may be unknown. Second, in order to do this map-reduce you need multiple versions of the state to exist... but the graph shares a common shared state, so how can this be?\n",
    "\n",
    "LangGraph supports this via the `Send` api. This can be used to allow a conditional edge to `Send` multiple different states to multiple nodes. The state it sends can be different from the state of the core graph.\n",
    "\n",
    "Let's see what this looks like in action! We'll put together a toy example of generating a list of words, and then writing a joke about each word, and then judging what the best joke is."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "0f0f78e4-423d-4e2d-aa1a-01efaec4715f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'generate_topics': {'subjects': ['cat', 'dog', 'rabbit', 'hamster', 'bird']}}\n",
      "{'generate_joke': {'jokes': ['Why did the rabbit go to the barber shop? Because it needed a hare cut!']}}\n",
      "{'generate_joke': {'jokes': ['Why was the cat sitting on the computer? Because it wanted to keep an eye on the mouse!']}}\n",
      "{'generate_joke': {'jokes': ['Why did the hamster join the band? Because it had great drumming skills!']}}\n",
      "{'generate_joke': {'jokes': [\"Why did the dog sit in the shade? Because he didn't want to be a hot dog!\"]}}\n",
      "{'generate_joke': {'jokes': ['Why did the bird join a band? Because it had the best tweet-talent!']}}\n",
      "{'best_joke': {'best_selected_joke': \"Why did the dog sit in the shade? Because he didn't want to be a hot dog!\"}}\n"
     ]
    }
   ],
   "source": [
    "import operator\n",
    "from typing import Annotated, TypedDict\n",
    "\n",
    "from langchain_core.pydantic_v1 import BaseModel\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "from langgraph.constants import Send\n",
    "from langgraph.graph import END, StateGraph\n",
    "\n",
    "# Model and prompts\n",
    "# Define model and prompts we will use\n",
    "subjects_prompt = \"\"\"Generate a comma separated list of between 2 and 5 {topic}.\"\"\"\n",
    "joke_prompt = \"\"\"Generate a joke about {subject}\"\"\"\n",
    "best_joke_prompt = \"\"\"Below are a bunch of jokes about {topic}. Select the best one! Return the ID of the best one.\n",
    "\n",
    "{jokes}\"\"\"\n",
    "\n",
    "\n",
    "class Subjects(BaseModel):\n",
    "    subjects: list[str]\n",
    "\n",
    "\n",
    "class Joke(BaseModel):\n",
    "    joke: str\n",
    "\n",
    "\n",
    "class BestJoke(BaseModel):\n",
    "    id: int\n",
    "\n",
    "\n",
    "model = ChatOpenAI()\n",
    "\n",
    "# Graph components: define the components that will make up the graph\n",
    "\n",
    "\n",
    "# This will be the overall state of the main graph.\n",
    "# It will contain a topic (which we expect the user to provide)\n",
    "# and then will generate a list of subjects, and then a joke for\n",
    "# each subject\n",
    "class OverallState(TypedDict):\n",
    "    topic: str\n",
    "    subjects: list\n",
    "    # Notice here we use the operator.add\n",
    "    # This is because we want combine all the jokes we generate\n",
    "    # from individual nodes back into one list - this is essentially\n",
    "    # the \"reduce\" part\n",
    "    jokes: Annotated[list, operator.add]\n",
    "    best_selected_joke: str\n",
    "\n",
    "\n",
    "# This will be the state of the node that we will \"map\" all\n",
    "# subjects to in order to generate a joke\n",
    "class JokeState(TypedDict):\n",
    "    subject: str\n",
    "\n",
    "\n",
    "# This is the function we will use to generate the subjects of the jokes\n",
    "def generate_topics(state: OverallState):\n",
    "    prompt = subjects_prompt.format(topic=state[\"topic\"])\n",
    "    response = model.with_structured_output(Subjects).invoke(prompt)\n",
    "    return {\"subjects\": response.subjects}\n",
    "\n",
    "\n",
    "# Here we generate a joke, given a subject\n",
    "def generate_joke(state: JokeState):\n",
    "    prompt = joke_prompt.format(subject=state[\"subject\"])\n",
    "    response = model.with_structured_output(Joke).invoke(prompt)\n",
    "    return {\"jokes\": [response.joke]}\n",
    "\n",
    "\n",
    "# Here we define the logic to map out over the generated subjects\n",
    "# We will use this an edge in the graph\n",
    "def continue_to_jokes(state: OverallState):\n",
    "    # We will return a list of `Send` objects\n",
    "    # Each `Send` object consists of the name of a node in the graph\n",
    "    # as well as the state to send to that node\n",
    "    return [Send(\"generate_joke\", {\"subject\": s}) for s in state[\"subjects\"]]\n",
    "\n",
    "\n",
    "# Here we will judge the best joke\n",
    "def best_joke(state: OverallState):\n",
    "    jokes = \"\\n\\n\".format()\n",
    "    prompt = best_joke_prompt.format(topic=state[\"topic\"], jokes=jokes)\n",
    "    response = model.with_structured_output(BestJoke).invoke(prompt)\n",
    "    return {\"best_selected_joke\": state[\"jokes\"][response.id]}\n",
    "\n",
    "\n",
    "# Construct the graph: here we put everything together to construct our graph\n",
    "graph = StateGraph(OverallState)\n",
    "graph.add_node(\"generate_topics\", generate_topics)\n",
    "graph.add_node(\"generate_joke\", generate_joke)\n",
    "graph.add_node(\"best_joke\", best_joke)\n",
    "graph.set_entry_point(\"generate_topics\")\n",
    "graph.add_conditional_edges(\"generate_topics\", continue_to_jokes)\n",
    "graph.add_edge(\"generate_joke\", \"best_joke\")\n",
    "graph.add_edge(\"best_joke\", END)\n",
    "app = graph.compile()\n",
    "\n",
    "\n",
    "# Call the graph: here we call it to generate a list of jokes\n",
    "for s in app.stream({\"topic\": \"animals\"}):\n",
    "    print(s)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "37ed1f71-63db-416f-b715-4617b33d4b7f",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
